package io.sundial.planning.quartz;

import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import io.sundial.core.context.Context;
import io.sundial.core.event.EventListener;
import io.sundial.core.event.EventSource;
import io.sundial.core.event.EventSourceSupplier;
import io.sundial.core.lifecycle.Stateful;
import io.sundial.core.lifecycle.exception.DestroyingException;
import io.sundial.core.lifecycle.exception.InitializingException;
import io.sundial.planning.Planner;
import io.sundial.planning.PlannerEvent;
import io.sundial.planning.event.PlannedEvent;
import io.sundial.planning.exception.ClearException;
import io.sundial.planning.exception.CloseException;
import io.sundial.planning.exception.PauseException;
import io.sundial.planning.exception.StartException;
import io.sundial.repository.RepositoryException;
import io.sundial.task.Task;
import org.quartz.*;
import org.quartz.spi.JobFactory;
import org.quartz.spi.TriggerFiredBundle;

import java.util.*;

/**
 * 缺省的规划者
 *
 * @author Payne 646742615@qq.com
 * 2018/12/21 14:21
 */
public class QuartzPlanner extends Stateful implements Planner, JobFactory, Job {
    private EventSource eventSource;
    private Scheduler scheduler;

    @Override
    protected void initializing(Context context) throws InitializingException {
        super.initializing(context);

        // 绑定事件源
        this.eventSource = eventSource != null
                ? eventSource
                : context.get(EventSource.class, new EventSourceSupplier());

        // 绑定调度器
        this.scheduler = scheduler != null
                ? scheduler
                : context.get(Scheduler.class, new SchedulerSupplier());
        try {
            this.scheduler.setJobFactory(this);
        } catch (SchedulerException e) {
            throw new InitializingException(e);
        }
    }

    @Override
    protected void destroying() throws DestroyingException {
        super.destroying();

        eventSource = null;

        try {
            scheduler.shutdown();
            scheduler = null;
        } catch (SchedulerException e) {
            throw new DestroyingException(e);
        }
    }

    private Plan transform(Task.Key key, Task task) {
        JobBuilder jobBuilder = JobBuilder.newJob()
                .ofType(QuartzPlanner.class)
                .withIdentity(key.getName(), key.getGroup())
                .withDescription(task.getDescription())
                .usingJobData("shardingTotal", task.getShardingTotal())
                .usingJobData("shardingTable", task.getShardingTable())
                .usingJobData("balanceAlgorithm", task.getBalanceAlgorithm())
                .usingJobData("misfireInstruction", task.getMisfireInstruction())
                .usingJobData("concurrentAllowed", task.isConcurrentAllowed())
                .usingJobData("jobName", task.getJobName())
                .usingJobData("jobGroup", task.getJobGroup())
                .usingJobData("children", Joiner.on(';').join(Optional.fromNullable(task.getChildren()).or(Collections.<Task.Key>emptySet())));

        Map<String, String> jobArguments = Optional.fromNullable(task.getJobArguments()).or(Collections.<String, String>emptyMap());
        for (Map.Entry<String, String> entry : jobArguments.entrySet()) {
            jobBuilder.usingJobData("jobArguments." + entry.getKey(), entry.getValue());
        }

        JobDetail jobDetail = jobBuilder.build();

        CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                .withSchedule(CronScheduleBuilder.cronSchedule(task.getCronExpression()))
                .withIdentity(key.getName(), key.getGroup())
                .forJob(jobDetail)
                .startAt(Optional.fromNullable(task.getDateStart()).or(new Date()))
                .endAt(task.getDateEnd())
                .withPriority(task.getPriority())
                .build();

        return new Plan(jobDetail, cronTrigger);
    }

    private Task transform(JobDetail jobDetail, CronTrigger cronTrigger) {
        JobKey jobKey = jobDetail.getKey();
        JobDataMap jobData = jobDetail.getJobDataMap();

        Task task = new Task();
        task.setName(jobKey.getName());
        task.setGroup(jobKey.getGroup());
        task.setCronExpression(cronTrigger.getCronExpression());
        task.setDescription(jobDetail.getDescription());
        task.setShardingTotal(jobData.getIntValue("shardingTotal"));
        task.setShardingTable(jobData.getString("shardingTable"));
        task.setBalanceAlgorithm(jobData.getString("balanceAlgorithm"));
        task.setMisfireInstruction(jobData.getString("misfireInstruction"));
        task.setConcurrentAllowed(jobData.getBooleanValue("concurrentAllowed"));
        task.setDateStart(cronTrigger.getStartTime());
        task.setDateEnd(cronTrigger.getEndTime());
        task.setPriority(cronTrigger.getPriority());
        task.setJobName(jobData.getString("jobName"));
        task.setJobGroup(jobData.getString("jobGroup"));

        task.setChildren(new LinkedHashSet<Task.Key>());
        String children = jobData.getString("children");
        Iterable<String> keys = children == null || children.isEmpty() ? Collections.<String>emptySet() : Splitter.on(';').split(children);
        for (String key : keys) {
            Task.Key child = new Task.Key(key.split("/")[1], key.split("/")[0]);
            task.getChildren().add(child);
        }

        Map<String, String> jobArguments = new LinkedHashMap<>();
        for (String argsKey : jobData.keySet()) {
            if (!argsKey.startsWith("jobArguments.")) {
                continue;
            }
            String argsName = argsKey.substring("jobArguments.".length());
            jobArguments.put(argsName, jobData.getString(argsKey));
        }
        task.setJobArguments(jobArguments);

        return task;
    }

    @Override
    public void execute(JobExecutionContext context) {
        JobDetail jobDetail = context.getJobDetail();
        Date date = context.getScheduledFireTime();
        long time = date.getTime();
        CronTrigger cronTrigger = (CronTrigger) context.getTrigger();
        Task task = transform(jobDetail, cronTrigger);
        eventSource.fire(new PlannedEvent(task, time));
    }

    @Override
    public Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) {
        return this;
    }

    @Override
    public void start() throws StartException {
        try {
            scheduler.start();
        } catch (SchedulerException e) {
            throw new StartException(e);
        }
    }

    @Override
    public void pause() throws PauseException {
        try {
            scheduler.standby();
        } catch (SchedulerException e) {
            throw new PauseException(e);
        }
    }

    @Override
    public void clear() throws ClearException {
        try {
            scheduler.clear();
        } catch (SchedulerException e) {
            throw new ClearException(e);
        }
    }

    @Override
    public void close() throws CloseException {
        try {
            scheduler.shutdown();
        } catch (SchedulerException e) {
            throw new CloseException(e);
        }
    }

    @Override
    public Task find(Task.Key key) {
        try {
            JobDetail jobDetail = scheduler.getJobDetail(JobKey.jobKey(key.getName(), key.getGroup()));
            CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(TriggerKey.triggerKey(key.getName(), key.getGroup()));

            if (jobDetail == null || cronTrigger == null) {
                return null;
            }

            return transform(jobDetail, cronTrigger);
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public void save(Task.Key key, Task task) throws RepositoryException {
        try {
            Plan plan = transform(key, task);
            scheduler.scheduleJob(plan.getJobDetail(), plan.getCronTrigger());
        } catch (SchedulerException e) {
            throw new RepositoryException(e);
        }
    }

    @Override
    public void replace(Task.Key key, Task task) throws RepositoryException {
        try {
            Plan plan = transform(key, task);
            scheduler.scheduleJob(plan.getJobDetail(), Collections.singleton(plan.getCronTrigger()), true);
        } catch (SchedulerException e) {
            throw new RepositoryException(e);
        }
    }

    @Override
    public Task remove(Task.Key key) {
        try {
            Task found = find(key);

            if (found == null) {
                return null;
            }

            JobKey jobKey = JobKey.jobKey(key.getName(), key.getGroup());
            boolean deleted = scheduler.deleteJob(jobKey);

            if (deleted) {
                return found;
            }

            return null;
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public boolean contains(Task.Key key) {
        try {
            return scheduler.checkExists(JobKey.jobKey(key.getName(), key.getGroup())) && scheduler.checkExists(TriggerKey.triggerKey(key.getName(), key.getGroup()));
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public Enumeration<Task.Key> keys() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Enumeration<Task> values() {
        throw new UnsupportedOperationException();
    }

    @Override
    public void addEventListener(EventListener<? extends PlannerEvent> listener) {
        eventSource.addEventListener(listener);
    }

    @Override
    public void removeEventListener(EventListener<? extends PlannerEvent> listener) {
        eventSource.removeEventListener(listener);
    }

}
